/**
 * Copyright (c) 2020 lizebang
 *
 * This software is released under the MIT License.
 * https://opensource.org/licenses/MIT
 */

package config

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strings"
	"sync"

	"gitee.com/banggua/ggtsync/logger"
	"github.com/go-playground/validator/v10"

	"gopkg.in/yaml.v2"
)

type client struct {
	config    config
	ins       []string
	tos       []string
	projects  map[string]Project
	locker    sync.Locker
	validator *validator.Validate
}

type config struct {
	filepath string
}

var defaultClient = &client{
	projects:  make(map[string]Project),
	locker:    &sync.Mutex{},
	validator: validator.New(),
}

const (
	defaultInterval int = 24
)

func (c *client) renew() {
	c.config = config{}
	c.ins = make([]string, 0)
	c.tos = make([]string, 0)
	c.projects = make(map[string]Project)
}

func (c *client) isIncludeValid(include string) bool {
	for _, in := range c.ins {
		if in == include {
			return false
		}
	}
	return true
}

func (c *client) isToValid(to string) bool {
	for _, t := range c.tos {
		if strings.HasPrefix(to, t) {
			return false
		}
	}
	return true
}

func (c *client) read(filename string, isConfig bool) ([]string, []Project, error) {
	if isConfig {
		c.config.filepath = filename
	}

	in, err := ioutil.ReadFile(filename)
	if err != nil {
		logger.Errorf("%v", err)
		return nil, nil, err
	}

	var f Fragment
	err = yaml.Unmarshal(in, &f)
	if err != nil {
		logger.Errorf("%v", err)
		return nil, nil, err
	}

	for key := range f.Projects {
		if f.Projects[key].Interval == 0 {
			f.Projects[key].Interval = defaultInterval
		}
	}

	err = c.validator.Struct(&f)
	if err != nil {
		logger.Errorf("%v", err)
		return nil, nil, err
	}

	for key := range f.Projects {
		var (
			orders  = []int{f.Projects[key].Order}
			plugins []Plugin
		)
		for _, pPlugin := range f.Projects[key].Plugins {
			var isExist = false
			for _, plugin := range plugins {
				if reflect.DeepEqual(pPlugin, plugin) {
					isExist = true
					break
				}
			}
			if !isExist {
				orders = append(orders, pPlugin.Order)
				plugins = append(plugins, pPlugin)
			}
		}
		sort.Ints(orders)
		od := orders[0]
		for index := 1; index < len(orders); index++ {
			if od >= orders[index] {
				return nil, nil, fmt.Errorf("%s project(%s): %w", filename, f.Projects[key].Name, ErrorConfigOrderInvalid)
			}
		}

		for _, plugin := range plugins {
			if plugin.Order < f.Projects[key].Order {
				f.Projects[key].before = append(f.Projects[key].before, plugin)
			} else {
				f.Projects[key].after = append(f.Projects[key].after, plugin)
			}
		}
	}

	return f.Includes, f.Projects, err
}

func (c *client) write(filename string) error {
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		logger.Errorf("%v", err)
		return err
	}
	defer file.Close()

	var projects []Project
	for _, pj := range c.projects {
		projects = append(projects, pj)
	}

	var f Fragment
	f.Projects = projects

	out, err := yaml.Marshal(&f)
	if err != nil {
		logger.Errorf("%v", err)
		return err
	}

	_, err = file.Write(out)
	if err != nil {
		logger.Errorf("%v", err)
		return err
	}
	return nil
}

func (c *client) load(config string) error {
	abspath, err := filepath.Abs(config)
	if err != nil {
		logger.Errorf("%v", err)
		return err
	}
	c.ins = append(c.ins, abspath)

	for index := 0; index < len(c.ins); index++ {
		includes, projects, err := c.read(c.ins[index], index == 0)
		if err != nil {
			return err
		}

		for _, i := range includes {
			absIn := i
			if !filepath.IsAbs(absIn) {
				absIn = filepath.Join(filepath.Dir(c.ins[index]), absIn)
			}
			if c.isIncludeValid(absIn) {
				c.ins = append(c.ins, absIn)
			}
		}

		for _, project := range projects {
			absTo := project.To
			if !filepath.IsAbs(absTo) {
				absTo = filepath.Join(filepath.Dir(c.ins[index]), absTo)
			}

			if !c.isToValid(absTo) {
				return fmt.Errorf("%s project(%s): %w", c.ins[index], project.Name, ErrorConfigPathToInvalid)
			}
			project.To = absTo
			c.tos = append(c.tos, absTo)

			if _, exist := c.projects[project.Name]; exist {
				return fmt.Errorf("%s project(%s): %w", c.ins[index], project.Name, ErrorConfigProjectExist)
			}
			c.projects[project.Name] = project
		}
	}

	return nil
}

func (c *client) Load(config string) error {
	c.locker.Lock()
	defer c.locker.Unlock()
	c.renew()
	return c.load(config)
}

func (c *client) Reload() error {
	return c.Load(c.config.filepath)
}

func (c *client) Export(config string) error {
	return c.write(config)
}

func (c *client) GetConfigPath() string {
	c.locker.Lock()
	defer c.locker.Unlock()

	return c.config.filepath
}

func (c *client) GetIncludes() []string {
	c.locker.Lock()
	defer c.locker.Unlock()

	return c.ins
}

func (c *client) GetProjects() []Project {
	c.locker.Lock()
	defer c.locker.Unlock()

	var projects []Project
	for _, pj := range c.projects {
		projects = append(projects, pj)
	}
	return projects
}
